package mbsl.extensions;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.prefs.Preferences;

import mbsl.system.Core;
import mbsl.system.Logger;
import mbsl.ui.dialogs.Dialogs;

/**
 * Automatically loads extensions
 * <p>
 * The extensions can be named by class in the extensions/extensions file, or
 * exported to a JAR which will be automatically loaded if placed in the
 * extensions folder.
 * <p>
 * All extensions must have a gateway class which implements
 * mbsl.extensions.Extension
 */
public class Loader {
	private Preferences prefs;
	private static Logger logger = new Logger(Loader.class);
	private Core mCore;
	private List<Extension> mExtensions;

	public Loader(Core pCore) {
		mCore = pCore;
		prefs = Preferences.userRoot().node("mbsl").node("extensions");
		mExtensions = new ArrayList<Extension>();
		Deque<Extension> extensions = new ArrayDeque<Extension>();

		String extensionFolder = prefs.get("folder", DEFAULT_EXTENSION_FOLDER);

		openBin(extensions, extensionFolder);
		openJars(extensions, extensionFolder);

		boolean loop = false;
		
		while (!loop && !extensions.isEmpty()) {
			Extension e = extensions.poll();
			loop = true;
			
			if (e.loadAfter() == null || e.loadAfter().length < 1) {
				loop = false;

				addAndInit(e);
			} else {
				if (dependenciesLoaded(mExtensions, e.loadAfter())) {
					loop = false;
					
					addAndInit(e);
				} else
					extensions.add(e);
			}
		}
		
		if (!extensions.isEmpty())
			for (Extension extension : extensions)
				logger.warning("   Failed to load %s, because its dependencies were not found", extension.getClass().getCanonicalName());
	}
	
	private void addAndInit(Extension pExtension) {
		mExtensions.add(pExtension);
		
		pExtension.init(mCore);
		
		logger.info(String.format("   Loaded %1$s", pExtension.getClass().getCanonicalName()));
	}
	
	private boolean dependenciesLoaded(List<Extension> pLoadedExtensions, String[] pDependencyNames) {
		for (String dependency : pDependencyNames) {
			boolean found = false;
			
			for (Extension extension : pLoadedExtensions) {
				if (extension.getClass().getCanonicalName().equals(dependency))
					found = true;
			}
			
			if (!found)
				return false;
		}
		
		return true;
	}
	
	public void start() {
		for (Extension e : mExtensions)
			e.start(mCore);
	}

	/** Sends shutdown to all loaded extensions. */
	public void shutDown() {
		for (Extension e : mExtensions)
			e.shutdown(mCore);
	}
	/** Load the extension classes directly from bin/extensions. */
	private void openBin(Deque<Extension> pExtensions, String pExtensionFolder) {
		File file = new File(String.format("%1$s/%1$s", pExtensionFolder));
		BufferedReader reader = null;
		List<String> classNames = null;

		try {
			if (!file.exists())
				return;
			
			reader = new BufferedReader(new FileReader(file));

			classNames = new ArrayList<String>();

			while (reader.ready())
				classNames.add(reader.readLine());
		} catch (Exception e) {
			throw new RuntimeException(String.format("Could not read %s.", file.getPath()));
		} finally {
			if (reader != null)
				try {
					reader.close();
				} catch (IOException e) {
				}
		}

		for (String className : classNames) {
			try {
				Object object = Class.forName(String.format("%s", className)).newInstance();

				pExtensions.add(Extension.class.cast(object));
			} catch (Exception e) {
				logger.warning(String.format("Could not load %s.", className));
			}
		}
	}

	/** Load the extensions classes from the extensions folder. */
	private void openJars(Deque<Extension> pExtensions, String pExtensionFolder) {
		File folder = new File(pExtensionFolder);
		List<String> classes = new ArrayList<String>();

		if (folder.exists() && folder.isFile()) {
			Dialogs.error(mCore.getMainController().getWindow(), "Failed to load extension folder because it appears to be a file.", "Failed to Load Extensions");

			prefs.remove("folder");

			return;
		}

		File[] jars = folder.listFiles(new FilenameFilter() {
			public boolean accept(File pFolder, String pName) {
				return pName.endsWith(".jar");
			}
		});

		if (jars == null)
			return;

		for (File jar : jars) {
			URLClassLoader classLoader = null;

			try {
				classLoader = URLClassLoader.newInstance(new URL[] { jar.toURI().toURL() });

				JarInputStream jarStream = new JarInputStream(new FileInputStream(jar));

				for (JarEntry e = jarStream.getNextJarEntry(); e != null; e = jarStream.getNextJarEntry()) {
					if (e.getName().endsWith(".class")) {
						String className = e.getName().substring(0, e.getName().length() - 6).replaceAll("/", "\\.");
						classes.add(className);
					}
				}

				for (String className : classes) {
					try {
						Class<?> jarClass = classLoader.loadClass(className);

						if (Extension.class.isAssignableFrom(jarClass))
							pExtensions.add((Extension)jarClass.newInstance());
					} catch (Exception e) {
						logger.warning(String.format("Failed to load %s.", className));
					}
				}
			} catch (Exception e) {
				logger.warning(String.format("Failed to read %s.", jar));
			}
		}
	}

	public static final String DEFAULT_EXTENSION_FOLDER = "extensions";
}
